/*
* Copyright (C) 2010-2012 Magnus Skjegstad
* 
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
* 
* The latest version of this library can be downloaded from http://mist-pubsub.googlecode.com
* 
*/
package com.skjegstad.mist2;

import com.skjegstad.mist2.datatypes.Acknowledgement;
import com.skjegstad.mist2.datatypes.BloomfilterAcknowledgment;
import com.skjegstad.mist2.datatypes.ConnectionIdentifier;
import com.skjegstad.mist2.datatypes.DataFragment;
import com.skjegstad.mist2.datatypes.NodeIdentifier;
import com.skjegstad.mist2.datatypes.TopicIdentifier;
import com.skjegstad.mist2.datatypes.TopicMap;
import com.skjegstad.mist2.events.RegisterDataFragmentEvent;
import com.skjegstad.mist2.events.RegisterNodeEvent;
import com.skjegstad.mist2.events.RemoveNodeEvent;
import com.skjegstad.mist2.exceptions.MistException;
import com.skjegstad.mist2.managers.MistEventManager;
import com.skjegstad.mist2.storage.DataFragmentStore;
import com.skjegstad.mist2.storage.KnownNodesStore;
import java.net.InetAddress;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 *
 * @author Magnus Skjegstad
 */
public class NetworkModuleHelper {
    private DataFragmentStore dataFragmentStore;
    private KnownNodesStore knownNodesStore;
    private MistEventManager mistEventManager;
    private MistCoreManager coreManager;
    private ConnectionIdentifier id;

    public NetworkModuleHelper(ConnectionIdentifier connectionIdentifier,
            int fragmentSize,
            MistCoreManager mistCoreManager,
            MistEventManager mistEventManager) throws MistException {

        this.id = connectionIdentifier;
        this.coreManager = mistCoreManager;
        this.dataFragmentStore = mistCoreManager.getDataStoreManager().createDataFragmentStore(id, fragmentSize);
        this.knownNodesStore = mistCoreManager.getKnownNodesStoreManager().createKnownNodesStore(id);
        this.mistEventManager = mistEventManager;
    }

    /**
     * Register neighbor node. Call this every time the node is updated (e.g. an ack or topic update is received). Also updates
     * the last seen value used by getExpiredNodes(). If the node is new, a NewNodeDiscoveredEvent is triggered. 
     *
     * @param nodeIdentifier
     * @param topics
     * @param acknowledgment
     */
    public void registerNeighborNode(NodeIdentifier nodeIdentifier,
                             TopicMap topicsWithTtl,
                             InetAddress ipAddress,
                             Acknowledgement acknowledgment){
        mistEventManager.getNodeManagerEventPipe().trigger(new RegisterNodeEvent(id, nodeIdentifier, ipAddress, topicsWithTtl, acknowledgment));
    }

    /**
     * Remove neighbor node, e.g. after it has timed out. This triggers a RemoveNodeEvent. Subsequent calls to
     * registerNeighborNode will trigger NewNodeDiscoveredEvent.
     *
     * @param nodeIdentifier
     */
    public void removeNeighborNode(NodeIdentifier nodeIdentifier) {
        removeNeighborNodes(Collections.singletonList(nodeIdentifier));
    }

    /**
     * Remove neighbor nodes, e.g. after they have timed out. Triggers a RemoveNodeEvent. Subsequent calls to
     * registerNeighborNode will trigger NewNodeDiscoveredEvent.
     *
     * @param nodeIdentifiers list of node identifiers
     */
    public void removeNeighborNodes(Collection<NodeIdentifier> nodeIdentifiers) {
        mistEventManager.getNodeManagerEventPipe().trigger(new RemoveNodeEvent(nodeIdentifiers, id));
    }

    /**
     * Get topics the given node subscribes to and TTLs.
     * 
     * @param nodeIdentifier
     * @return
     */
    public TopicMap getTopicsWithTtl(NodeIdentifier nodeIdentifier) {
        return knownNodesStore.getTopicsWithTtl(nodeIdentifier);
    }

    /**
     * Get topics the given node subscribes to.
     * 
     * @param nodeIdentifier
     * @return
     */
    public Collection<TopicIdentifier> getTopics(NodeIdentifier nodeIdentifier) {
        TopicMap topics = knownNodesStore.getTopicsWithTtl(nodeIdentifier);
        if (topics == null)
            return null; 
        return topics.keySet();
    }

    /**
     * Get all neighboring nodes.
     *
     * @return
     */
    public Collection<NodeIdentifier> getNodes() {
        return knownNodesStore.getNodes();
    }

    /**
     * Get list of expired neighbor nodes.
     * 
     * @param maxAge
     * @return
     */
    public Collection<NodeIdentifier> getExpiredNodes(long maxAge) {
        return knownNodesStore.getExpiredNodes(maxAge);
    }

    /**
     * Get the timestamp for the last time the node was seen.
     *
     * @param node
     * @return
     */
    public long getLastSeen(NodeIdentifier node) {
        return knownNodesStore.getLastSeen(node);
    }

    /**
     * Get all data fragment hashes matching any of the specified topics.
     *
     * @param topics Topics to match. May include wildcards.
     * @param excludeExpired exclude expired hashes
     * @param excludeInvalid  exclude invalid hashes (still ack'ed, but not in use anymore)
     * @return
     */
    public Collection<Long> getHashesToAck(Collection<TopicIdentifier> topics, boolean excludeExpired, boolean excludeInvalid) {
        return dataFragmentStore.getHashesToAck(topics, excludeExpired, excludeInvalid).keySet();
    }

    /**
     * Get all data fragments that matches the collection of hashes. If a hash matches a complete element all fragments in this element are returned.
     * 
     * @param hashes
     * @param removeExpired if expired data elements should be removed from the result.
     * @param excludeInvalid if invalid data elements should be removed from the result. These are results that are not expired, but not in use anymore.
     * @return
     * @throws MistException
     */
    public Collection<DataFragment> getDataFragments(Collection<Long> hashes, boolean removeExpired, boolean excludeInvalid) throws MistException {
        return dataFragmentStore.getDataFragments(hashes, removeExpired, excludeInvalid);
    }

    /**
     * Get hashes not acked by a specific node. Data elements created by the node is not included even if
     * they are not included in the current ack.
     *
     * @param node
     * @param excludeExpired exclude expired hashes
     * @param excludeInvalid  exclude invalid data elements.
     * @return
     */
    public Collection<Long> getHashesMissingInNode(NodeIdentifier node, boolean excludeExpired, boolean excludeInvalid) {
        return dataFragmentStore.getHashesNotInAck(knownNodesStore.getAcknowledgment(node), this.getTopics(node), node, excludeExpired, excludeInvalid).keySet();
    }

    /**
     * Get hashes not acked by any node.
     * @param excludeExpired exclude expired hashes
     * @param excludeInvalid exclude invalid data elements
     * @return
     */
    public Collection<Long> getHashesNotAckedByAnyNode(boolean excludeExpired, boolean excludeInvalid) {
        Set<Long> hashes = new HashSet();

        for (NodeIdentifier n : knownNodesStore.getNodes())
            hashes.addAll(this.getHashesMissingInNode(n, excludeExpired, excludeInvalid));

        return hashes;
    }

    /**
     * Get hashes not acked by any node, but exclude some nodes.
     *
     * @param excludeNodes nodes to exclude.
     * @param excludeExpired exclude expired hashes
     * @param excludeInvalid exclude invalid data elements
     * @return
     */
    public Collection<Long> getHashesNotAckedByAnyNode(List<NodeIdentifier> excludeNodes, boolean excludeExpired, boolean excludeInvalid) {
        Set<Long> hashes = new HashSet();

        for (NodeIdentifier n : knownNodesStore.getNodes()) {
            if (excludeNodes.contains(n))
                continue;
            hashes.addAll(this.getHashesMissingInNode(n, excludeExpired, excludeInvalid));
        }

        return hashes;
    }

    /**
     * Get instance of event manager. Use this to receive or trigger events.
     *
     * @return
     */
    public MistEventManager getMistEventManager() {
        return mistEventManager;
    }

    /**
     * Get active topics. This includes our own topics + topics from neighbors decremented by 1.
     * 
     * @return
     */
    public TopicMap getActiveTopics() {
        return coreManager.getActiveTopics(id);
    }

    /**
     * Get maximum age of data elements before they should be forced to expire.
     * 
     * @return
     */
    public int getMaximumAge() {
        return coreManager.getMaximumDataElementAge();
    }

    /**
     * Get maximum number of extra bits to use for Bloom filter.
     *
     * @return
     */
    public byte getMaximumExtraBits() {
        return coreManager.getMaximumExtraBloomfilterBits();
    }

    /**
     * Log message.
     *
     * @param msg
     */
    public void log(String msg) {
        mistEventManager.log(msg);        
    }

    /**
     * Log debug message.
     *
     * @param msg
     */
    public void debug(String msg) {
        MistUtil.debug(getMyNodeIdentifier(), msg);
    }

    /**
     * Get current node identifier.
     *
     * @return node identifier.
     */
    public NodeIdentifier getMyNodeIdentifier() {
        return coreManager.getMyNodeId();
    }

    /**
     * Add new data fragments to the data store. If new data fragments are added with this method, a
     * NewDataFragmentReceived is triggered. If the data fragments already exists, no events are
     * triggered.
     *
     * @param fragments data fragments to store.
     */
    public void addDataFragments(List<DataFragment> fragments) {
        mistEventManager.getDataFragmentEventPipe(id).trigger(new RegisterDataFragmentEvent(id, fragments));
    }

}
